#pragma once

#include <memory>
#include <cassert>
#include "messaging/target.h"
#include "messaging/transport.h"
#include "messaging/exception.h"


namespace evm { namespace messaging {

using namespace std;

struct CallContext;
using CallContextPtr = shared_ptr<CallContext>;

struct CallContext {
    TransportPtr transport;
    TargetPtr target;
    int timeout;
    int retry;

    CallContext(TransportPtr transport, TargetPtr target, int timeout, int retry)
    : transport(transport), target(target), timeout(timeout), retry(retry) {}

    void cast(const string& method, const Json& args = nullptr) {
        auto msg = make_message(method, args);
        transport->send(target, std::move(msg), false, -1, retry);
    }

    Json call(const string& method, const Json& args = nullptr) {
        auto msg = make_message(method, args);
        return transport->send(target, std::move(msg), true, timeout, retry);
    }

    CallContextPtr prepare(const Json& vars = Json::object()) {
        return prepare(*this, vars);
    }

private:
    Json make_message(const string& method, const Json& args = nullptr) {
        Json msg = {
            {"method", method},
            {"args", args},
            {"namespace", target->namespace_}
        };
        return msg;
    }

    template <typename Base>
    static CallContextPtr prepare(const Base& base, const Json& vars) {
        /* Propagate new target fields. */
        auto target = (*base.target)(vars);

        return make_shared<CallContext>(base.transport, target,
                                        /* Propagate new callcontext fields. */
                                        vars.count("timeout") ? vars["timeout"].get<int>() : base.timeout,
                                        vars.count("retry") ? vars["retry"].get<int>() : base.retry);
    }

    friend struct RPCClient;
};

struct RPCClient {
    /* Messaging transport. */
    TransportPtr transport;

    /* The default target for invocations. */
    TargetPtr target;

    /* Optional default timeout (in seconds) for call()s. */
    int timeout;

    /* Optional default connection retries configuration.
     * -1 means to retry forever (default)
     * 0 means no retry
     * N means N retries */
    int retry;

    /* Construct a rpc client.
     * `timeout`: if < 0, no timeout; if > 0, timeout value; if 0, use config option `rpc_response_timeout`.
     * `retry`: if < 0, retry forever; if > 0, retry so many times after timeout; if 0, no retry.
     * Only meaningful when `timeout` > 0. */
    RPCClient(TransportPtr transport, TargetPtr target, int timeout = 0, int retry = 0)
        : transport(transport), target(target),
          timeout(timeout != 0 ? timeout : (*transport->conf)["rpc_response_timeout"].get<int>()),
          retry(retry < 0 ? -1 : retry) {}

    /* Return a custom call context, with optional specified topic, namespace, fanout, timeout, retry. */
    CallContextPtr prepare(const Json& vars = Json::object()) {
        return CallContext::prepare(*this, vars);
    }

    void cast(const string& method, const Json& args = nullptr) {
        prepare()->cast(method, args);
    }

    Json call(const string& method, const Json& args = nullptr) {
        return prepare()->call(method, args);
    }
};

using RPCClientPtr = shared_ptr<RPCClient>;

}}
